/**
 * Copyright (c) 2016-2023, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jpress.module.article.controller.admin;

import com.jfinal.aop.Inject;
import com.jfinal.kit.LogKit;
import com.jfinal.kit.Ret;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.upload.UploadFile;
import io.jboot.db.model.Columns;
import io.jboot.utils.*;
import io.jboot.web.controller.annotation.RequestMapping;
import io.jboot.web.validate.EmptyValidate;
import io.jboot.web.validate.Form;
import io.jpress.JPressConsts;
import io.jpress.JPressOptions;
import io.jpress.commons.flowable.FlowConsts;
import io.jpress.commons.layer.SortKit;
import io.jpress.commons.service.SiteModelProxy;
import io.jpress.commons.word.LibreOfficeUtil;
import io.jpress.commons.word.POIUtil;
import io.jpress.commons.wordsfilter.WordsFilterUtil;
import io.jpress.commons.wordsfilter.algorithm.DFAMatch;
import io.jpress.core.editor.EditorManager;
import io.jpress.core.flowable.FlowableUtil;
import io.jpress.core.menu.annotation.AdminMenu;
import io.jpress.core.template.Template;
import io.jpress.core.template.TemplateManager;
import io.jpress.model.Department;
import io.jpress.model.FlowTemplate;
import io.jpress.model.Menu;
import io.jpress.module.article.ArticleFields;
import io.jpress.module.article.ArticlePermissionGroups;
import io.jpress.module.article.flowable.ArticleFlowConsts;
import io.jpress.module.article.model.Article;
import io.jpress.module.article.model.ArticleCategory;
import io.jpress.module.article.model.ArticleFlowStatus;
import io.jpress.module.article.model.ArticleImage;
import io.jpress.module.article.service.ArticleCategoryService;
import io.jpress.module.article.service.ArticleFlowStatusService;
import io.jpress.module.article.service.ArticleImageService;
import io.jpress.module.article.service.ArticleService;
import io.jpress.module.article.service.search.ArticleSearcherFactory;
import io.jpress.permission.annotation.NeedPermission;
import io.jpress.permission.annotation.PermissionDef;
import io.jpress.service.DepartmentService;
import io.jpress.service.FlowTemplateService;
import io.jpress.service.MenuService;
import io.jpress.service.UserService;
import io.jpress.web.base.AdminControllerBase;
import org.apache.commons.lang3.ArrayUtils;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;

import javax.validation.constraints.NotNull;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Michael Yang 杨福海 （fuhai999@gmail.com）
 * @version V1.0
 */
@RequestMapping(value = "/admin/article", viewPath = JPressConsts.DEFAULT_ADMIN_VIEW)
public class _ArticleController extends AdminControllerBase {

    @Inject
    private ArticleService articleService;

    @Inject
    private ArticleCategoryService categoryService;

    @Inject
    private DepartmentService departmentService;

    @Inject
    private MenuService menuService;

    @Inject
    private UserService userService;

    @Inject
    private FlowTemplateService flowTemplateService;

    @Inject
    private ArticleFlowStatusService articleFlowStatusService;

    @Inject
    private ArticleImageService imageService;


    @PermissionDef(title = "查看文章列表", groupId = ArticlePermissionGroups.ARTICLE)
    @AdminMenu(text = "文章管理", groupId = "article", order = 0)
    public void list() {

        String status = getPara("status");
        String title = getPara("title");
        Long categoryId = getParaToLong("categoryId");

        Long userId = getParaToLong("userId");

        Date startDate = null;
        Date endDate = null;
        String dateRange = getPara("dateRange");
        if (StrUtil.isNotBlank(dateRange) && dateRange.contains("~")) {
            String[] startAndEndDateStrings = dateRange.split("~");
            startDate = DateUtil.parseDate(startAndEndDateStrings[0].trim());
            endDate = DateUtil.parseDate(startAndEndDateStrings[1].trim());
        }


        Columns baseColumns = Columns.create();

        if (startDate != null && endDate != null) {
            baseColumns.gt("article.created", startDate);
            baseColumns.le("article.created", DateUtil.addDays(endDate, 1));
        }

        baseColumns.eq("article.user_id", userId);

        if (StrUtil.isBlank(status)) {
            baseColumns.ne("article.status", Article.STATUS_TRASH);
        } else {
            baseColumns.eq("article.status", status);
        }


        //当前用户归属的部门ID
        Long departmentId = userService.findDepartmentId(getLoginedUser().getId());
        if (departmentId == null) {
            render("article/article_list.html");
            return;
        }

        //总部用户，查询所有文章，包括没有归类的文章 和 已经归类，但是文章类型被删除的文章
        if (departmentId == 1L) {
            findAllDatas(baseColumns, categoryId, title);
        }
        //非总部用户，只能查看其有权限的分类下的文章
        else {
            findDepartmentDatas(departmentId, baseColumns, categoryId, title);
        }


        render("article/article_list.html");
    }


    private void findAllDatas(Columns baseColumns, Long categoryId, String title) {

        baseColumns.eq("mapping.category_id", categoryId);
        baseColumns.likeAppendPercent("article.title", title);


        Page<Article> page = articleService._paginateByColumns(getPagePara(), getPageSizePara(), baseColumns, null);
        setAttr("page", page);

        Long draftCount = articleService.findCountByStatus(Article.STATUS_DRAFT, null);
        Long trashCount = articleService.findCountByStatus(Article.STATUS_TRASH, null);
        Long normalCount = articleService.findCountByStatus(Article.STATUS_NORMAL, null);

        Long underReviewCount = articleService.findCountByStatus(Article.STATUS_UNDER_REVIEW, null);
        Long rebutCount = articleService.findCountByStatus(Article.STATUS_REBUT, null);
        Long refuseCount = articleService.findCountByStatus(Article.STATUS_REFUSE, null);

        setAttr("draftCount", draftCount);
        setAttr("trashCount", trashCount);
        setAttr("normalCount", normalCount);

        setAttr("underReviewCount", underReviewCount);
        setAttr("rebutCount", rebutCount);
        setAttr("refuseCount", refuseCount);
        setAttr("totalCount", draftCount + trashCount + normalCount + underReviewCount + rebutCount + refuseCount);


        List<ArticleCategory> categories = categoryService.findListByType(ArticleCategory.TYPE_CATEGORY);
        SortKit.toLayer(categories);
        setAttr("categories", categories);

        flagCheck(categories, categoryId);
    }


    private void findDepartmentDatas(Long departmentId, Columns baseColumns, Long categoryId, String title) {

        //当前部门绑定额文章分类ID
        List<Long> departmentCategoryIds = categoryService.findIdsByDepartmentId(departmentId);
        if (departmentCategoryIds == null || departmentCategoryIds.isEmpty()) {
            return;
        }

        setAttr("departmentCategoryIds", departmentCategoryIds);


        //必须保证 categoryId 是当前部门下绑定的 ID
        if (categoryId != null && departmentCategoryIds.contains(categoryId)) {
            baseColumns.eq("mapping.category_id", categoryId);
        } else {
            baseColumns.in("mapping.category_id", departmentCategoryIds);
        }

        baseColumns.likeAppendPercent("article.title", title);


        Page<Article> page = articleService._paginateByColumns(getPagePara(), getPageSizePara(), baseColumns, null);
        setAttr("page", page);

        Long draftCount = articleService.findCountByStatus(Article.STATUS_DRAFT, departmentCategoryIds);
        Long trashCount = articleService.findCountByStatus(Article.STATUS_TRASH, departmentCategoryIds);
        Long normalCount = articleService.findCountByStatus(Article.STATUS_NORMAL, departmentCategoryIds);

        Long underReviewCount = articleService.findCountByStatus(Article.STATUS_UNDER_REVIEW, departmentCategoryIds);
        Long rebutCount = articleService.findCountByStatus(Article.STATUS_REBUT, departmentCategoryIds);
        Long refuseCount = articleService.findCountByStatus(Article.STATUS_REFUSE, departmentCategoryIds);

        setAttr("draftCount", draftCount);
        setAttr("trashCount", trashCount);
        setAttr("normalCount", normalCount);

        setAttr("underReviewCount", underReviewCount);
        setAttr("rebutCount", rebutCount);
        setAttr("refuseCount", refuseCount);
        setAttr("totalCount", draftCount + trashCount + normalCount + underReviewCount + rebutCount + refuseCount);


        List<ArticleCategory> categories = categoryService.findListByType(ArticleCategory.TYPE_CATEGORY);
        SortKit.toLayer(categories);
        categories.removeIf(articleCategory -> !departmentCategoryIds.contains(articleCategory.getId()));
        setAttr("categories", categories);

        flagCheck(categories, categoryId);
    }


    @PermissionDef(title = "复制文章", groupId = ArticlePermissionGroups.ARTICLE)
    public void doCopy(@NotNull Integer articleId) {

        Long currentUserDepartmentId = userService.findDepartmentId(getLoginedUser().getId());
        if (currentUserDepartmentId == null) {
            renderFailJson("文章不存在或者没有权限");
            return;
        }

        Article article = articleService.findById(articleId);
        if (article == null) {
            renderFailJson("文章不存在或者没有权限");
            return;
        }

        //文章的分类
        List<ArticleCategory> articleCategories = categoryService.findListByArticleId(articleId);
        if (articleCategories == null || articleCategories.isEmpty()) {
            renderFailJson("文章不存在或者没有权限");
            return;
        }

        //当前用户可以管理或查看的文章分类ID
        List<Long> departmentCategoryIds = categoryService.findIdsByDepartmentId(currentUserDepartmentId);
        if (departmentCategoryIds == null || departmentCategoryIds.isEmpty()) {
            renderFailJson("文章不存在或者没有权限");
            return;
        }

        boolean hasPermission = false;
        for (ArticleCategory articleCategory : articleCategories) {
            if (departmentCategoryIds.contains(articleCategory.getId())) {
                hasPermission = true;
                break;
            }
        }

        if (!hasPermission) {
            renderFailJson("文章不存在或者没有权限");
            return;
        }

        //移除 ID 后保存，等同于新建文章。
        article.remove("id");
        article.setCreated(new Date());
        article.setModified(new Date());
        article.setStatus(Article.STATUS_DRAFT);
        article.setUserId(getLoginedUser().getId());

        Long newArticleId = (Long) articleService.save(article);


        Long[] allCategoryIds = new Long[articleCategories.size()];
        for (int i = 0; i < articleCategories.size(); i++) {
            allCategoryIds[i] = articleCategories.get(i).getId();
        }

        //设置分类
        articleService.doUpdateCategorys(newArticleId, allCategoryIds);


        if (allCategoryIds != null && allCategoryIds.length > 0) {
            for (Long categoryId : allCategoryIds) {
                categoryService.doUpdateArticleCount(categoryId);
            }
        }

        renderJson(Ret.ok().set("id", newArticleId));
    }


    @PermissionDef(title = "写文章", groupId = ArticlePermissionGroups.ARTICLE)
    @AdminMenu(text = "写文章", groupId = "article", order = 1)
    public void write() {

        setAttr("fields", ArticleFields.me());
        setAttr("allowEdit", true);

        List<ArticleCategory> categories = categoryService.findListByType(ArticleCategory.TYPE_CATEGORY);
        SortKit.toLayer(categories);


        if (categories != null && categories.size() > 0) {
            //当前用户归属的部门ID
            Long userDeptId = userService.findDepartmentId(getLoginedUser().getId());

            //如果当前用户不属于任何部门
            if (userDeptId == null) {
                //移除所有绑定了部门的分类
                categories.removeIf(articleCategory -> {
                    List<Long> departmentIds = categoryService.findDepartmentIds(articleCategory.getId());
                    return departmentIds != null && departmentIds.size() > 0;
                });
            }
            //如果当前用户已经有部门归属
            else {
                //查询该部门允许编辑的文章分类
                List<Long> departmentCategoryIds = categoryService.findIdsByDepartmentId(userDeptId);
                //移除所有绑定了部门的分类
                categories.removeIf(articleCategory -> {
                    List<Long> departmentIds = categoryService.findDepartmentIds(articleCategory.getId());
                    if (departmentIds == null || departmentIds.size() == 0) {
                        return false;
                    }

                    //删除其他所有已经配置了归属的分类
                    if (departmentCategoryIds == null || departmentCategoryIds.size() == 0) {
                        return true;
                    }
                    return !departmentCategoryIds.contains(articleCategory.getId());
                });
            }
        }

        setAttr("categories", categories);

        Article editArticle = articleService.findById(getParaToInt());
        if (editArticle != null) {


            setAttr("article", editArticle);

            List<ArticleCategory> tags = categoryService.findTagListByArticleId(editArticle.getId());
            setAttr("tags", tags);

            Long[] categoryIds = categoryService.findCategoryIdsByArticleId(editArticle.getId());
            flagCheck(categories, categoryIds);

            List<ArticleImage> articleImages = imageService.findListByArticleId(editArticle.getId());
            if (articleImages != null) {
                for (ArticleImage articleImage : articleImages) {
                    setAttr("ext_image_" + articleImage.getOrderNumber(), articleImage.getSrc());
                }
            }


            //文章还在审核中
            if (editArticle.isUnderReview()) {

                List<ArticleFlowStatus> flowStatuses = articleFlowStatusService.findListByArticleId(editArticle.getId());
                if (flowStatuses == null || !isAllowEditStatus(flowStatuses)) {
                    setAttr("allowEdit", false);
                }
            }
        }

        //初始化文章的编辑模式（编辑器）
        String editMode = editArticle == null ? getCookie(JPressConsts.COOKIE_EDIT_MODE) : editArticle.getEditMode();
        if (editMode == null) {
            editMode = JPressConsts.EDIT_MODE_HTML;
        }

        EditorManager.me().useEditorByEditMode(editMode);


        //设置文章当前的样式
        initStylesAttr("article_");

        render("article/article_write.html");
    }

    private boolean isAllowEditStatus(List<ArticleFlowStatus> flowStatuses) {
        for (ArticleFlowStatus flowStatus : flowStatuses) {
            if (flowStatus != null && flowStatus.isRebut()) {
                return true;
            }
        }
        return false;
    }


    @EmptyValidate({
            @Form(name = "id", message = "文章ID不能为空"),
            @Form(name = "mode", message = "文章编辑模式不能为空")
    })
    public void doChangeEditMode() {
        Long id = getParaToLong("id");
        String mode = getPara("mode");

        Article article = articleService.findById(id);
        if (article == null) {
            renderFailJson();
            return;
        }

        article.setEditMode(mode);
        articleService.update(article);
        renderOkJson();
    }

    private void initStylesAttr(String prefix) {
        Template template = TemplateManager.me().getCurrentTemplate();
        if (template == null) {
            return;
        }
        setAttr("flags", template.getFlags());
        List<String> styles = template.getSupportStyles(prefix);
        setAttr("styles", styles);
    }

    private void flagCheck(List<ArticleCategory> categories, Long... checkIds) {
        if (checkIds == null || checkIds.length == 0
                || categories == null || categories.size() == 0) {
            return;
        }

        for (ArticleCategory category : categories) {
            for (Long id : checkIds) {
                if (id != null && id.equals(category.getId())) {
                    category.put("isCheck", true);
                }
            }
        }
    }


    public void doImportWord() {
        boolean wordImportEnable = JPressOptions.getAsBool("article_word_import_enable");
        if (!wordImportEnable) {
            renderFailJson("Word 导入功能未开启.");
            return;
        }

        UploadFile file = getFile();
        if (file == null) {
            renderFailJson("未传入任何文件.");
            return;
        }


        String suffix = FileUtil.getSuffix(file.getFileName());
        if (!".doc".equalsIgnoreCase(suffix) && !".docx".equalsIgnoreCase(suffix)) {
            FileUtil.delete(file);
            renderFailJson("word 导入只支持 .doc 或者 .docx 文件.");
            return;
        }


        String importType = JPressOptions.get("article_word_import_type");
        if ("libreoffice".equals(importType)) {
            String officeHome = JPressOptions.get("article_word_import_libreoffice_path");
            if (StrUtil.isBlank(officeHome)) {
                FileUtil.delete(file);
                renderFailJson("LibreOffice 安装路径未配置.");
                return;
            }

            String wordHtml = LibreOfficeUtil.parse(file.getFile());
            render(Ret.ok().set("html", wordHtml));
        }
        //poi 导入
        else {
            String wordHtml = POIUtil.parse(file.getFile());
            render(Ret.ok().set("html", wordHtml));
        }


        if (file.getFile().exists() && !file.getFile().delete()) {
            LogKit.error("Can not delete file {}", file.getFile().getAbsolutePath());
        }

    }


    @NeedPermission("./write")
    @EmptyValidate({
            @Form(name = "article.title", message = "标题不能为空"),
            @Form(name = "article.content", message = "文章内容不能为空")
    })
    public void doWriteSave() {

        Article article = getModel(Article.class, "article");

        if (article.getId() != null) {
            Article existModel = articleService.findById(article.getId());
            if (existModel != null && existModel.isUnderReview()) {
                renderFailJson("保存失败，当前文章正在审核中...");
                return;
            }
        }

        //重置最后的审核评论
        article.putOption("lastFlowComment","");


        //对前端传入的 状态值设置不可信。
        article.remove("status");

        //默认情况下，请求会被 escape，通过 getOriginalPara 获得非 escape 的数据
        article.setContent(getCleanedOriginalPara("article.content"));


        Ret validRet = validateSlug(article);
        if (validRet.isFail()) {
            renderJson(validRet);
            return;
        }


        if (StrUtil.isNotBlank(article.getSlug())) {
            Article existArticle = articleService.findFirstBySlug(article.getSlug());
            if (existArticle != null && !existArticle.getId().equals(article.getId())) {
                renderJson(Ret.fail("message", "该固定链接已经存在"));
                return;
            }
        }

        if (article.getCreated() == null) {
            article.setCreated(new Date());
        }

        if (article.getModified() == null) {
            article.setModified(new Date());
        }

        if (article.getOrderNumber() == null) {
            article.setOrderNumber(0);
        }

        Long[] newCategoryIds = getParaValuesToLong("category");
        if (newCategoryIds == null || newCategoryIds.length == 0) {
            renderFailJson("请选择文章分类");
            return;
        }

        Long[] oldCategoryIds = null;
        if (article.getId() != null) {
            oldCategoryIds = categoryService.findCategoryIdsByArticleId(article.getId());
        }

        //用户操作按钮
        String action = get("action", "draft");

        //需要开启的审核流程
        Set<FlowTemplate> needStartFlows = new HashSet<>();

        //需要停止的审核流程
        Set<FlowTemplate> needStopFlows = new HashSet<>();

        if ("draft".equals(action)) {
            article.setStatus(Article.STATUS_DRAFT);
        } else if ("trash".equals(action)) {
            article.setStatus(Article.STATUS_TRASH);
        }
        //发布文章
        else if ("normal".equals(action)) {
            ArticleCategory[] newCategories = new ArticleCategory[newCategoryIds.length];
            for (int i = 0; i < newCategoryIds.length; i++) {
                newCategories[i] = categoryService.findById(newCategoryIds[i]);
            }
            for (ArticleCategory articleCategory : newCategories) {
                if (articleCategory != null && articleCategory.getFlowTemplateId() != null) {
                    FlowTemplate flowTemplate = flowTemplateService.findById(articleCategory.getFlowTemplateId());
                    if (flowTemplate != null && flowTemplate.isDeployed()) {
                        needStartFlows.add(flowTemplate);
                    }
                }
            }


            if (oldCategoryIds != null) {
                ArticleCategory[] oldCategories = new ArticleCategory[oldCategoryIds.length];
                for (int i = 0; i < oldCategoryIds.length; i++) {
                    oldCategories[i] = categoryService.findById(oldCategoryIds[i]);
                }
                for (ArticleCategory articleCategory : oldCategories) {
                    if (articleCategory != null && articleCategory.getFlowTemplateId() != null) {
                        FlowTemplate flowTemplate = flowTemplateService.findById(articleCategory.getFlowTemplateId());
                        if (flowTemplate != null && !needStartFlows.contains(flowTemplate)) {

                            //需要停止的审核流程
                            needStopFlows.add(flowTemplate);
                        }
                    }
                }
            }
        }


        //发布文章
        long id = (long) articleService.saveOrUpdate(article);
        articleService.doUpdateCommentCount(id);

        for (int orderNumber = 1; orderNumber <= 6; orderNumber++) {
            String src = getPara("ext_image_" + orderNumber);
            imageService.saveOrUpdateByArticleId(id, src, orderNumber);
        }


        for (FlowTemplate flowTemplate : needStartFlows) {
            if (tryToStartFlowInstance(flowTemplate, article)) {
                //设置文章为审核中...
                article.setStatus(Article.STATUS_UNDER_REVIEW);
            }
        }

        // 若分类下，没有绑定任何审核流程
        // 或者绑定的流程执行失败（比如流程已经下线，或被删除等）
        // 则直接发布成功。
        if (article.getStatus() == null) {
            article.setStatus(Article.STATUS_NORMAL);
        }

        //更新文章状态
        articleService.saveOrUpdate(article);

        for (FlowTemplate flowTemplate : needStopFlows) {
            tryToStopFlowInstance(flowTemplate, article);
        }

        setAttr("articleId", id);
        setAttr("article", article);


        Long[] tagIds = getTagIds(getParaValues("tag"));
        Long[] allCategoryIds = ArrayUtils.addAll(newCategoryIds, tagIds);

        //更新文章分类
        articleService.doUpdateCategorys(id, allCategoryIds);


        if (allCategoryIds != null && allCategoryIds.length > 0) {
            for (Long categoryId : allCategoryIds) {
                categoryService.doUpdateArticleCount(categoryId);
            }
        }

        if (oldCategoryIds != null && oldCategoryIds.length > 0) {
            for (Long categoryId : oldCategoryIds) {
                categoryService.doUpdateArticleCount(categoryId);
            }
        }

        Ret ret = id > 0 ? Ret.ok().set("id", id) : Ret.fail();

        List<DFAMatch> dfaMatches = WordsFilterUtil.getDfaMatches(article.getText());
        if (dfaMatches != null && !dfaMatches.isEmpty()) {
            StringBuilder matchedWords = new StringBuilder();
            for (int i = 0; i < dfaMatches.size(); i++) {
                matchedWords.append(dfaMatches.get(i).getWord());
                if (i != dfaMatches.size() - 1) {
                    matchedWords.append(", ");
                }
            }
            LogKit.error("用户ID:{}（昵称：{}），发布的文章《{}》包含敏感词：{}"
                    , getLoginedUser().getId(), getLoginedUser().getNickname(), article.getTitle(), matchedWords);
            ret.set("sensitiveWords", matchedWords);
        }

        renderJson(ret);
    }


    /**
     * 停止流程实例
     *
     * @param flowTemplate
     * @param article
     */
    private void tryToStopFlowInstance(FlowTemplate flowTemplate, Article article) {
        List<ArticleFlowStatus> articleFlowStatuses = articleFlowStatusService.findListByArticleId(article.getId());
        if (articleFlowStatuses != null) {
            for (ArticleFlowStatus articleFlowStatus : articleFlowStatuses) {
                articleFlowStatus.setStatus(ArticleFlowStatus.STATUS_CANCEL_WITH_FINISHED);
                articleFlowStatusService.update(articleFlowStatus);

                try {
                    RuntimeService runtimeService = FlowableUtil.getRuntimeService();
                    ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                            .processInstanceId(articleFlowStatus.getFlowProcessInstanceId())
                            .singleResult();
                    if (processInstance != null){
                        runtimeService.deleteProcessInstance(articleFlowStatus.getFlowProcessInstanceId(), "文章重新编辑后，未选择该流程的分类。");
                    }
                } catch (Exception e) {
                    LogKit.error(e.toString(), e);
                }
            }
        }
    }

    /**
     * 启动审核流程实例
     *
     * @param flowTemplate
     * @param article
     */
    private boolean tryToStartFlowInstance(FlowTemplate flowTemplate, Article article) {

        //在重新编辑文章，然后重新发布的场景下，该文章可能已经存在流程状态
        List<ArticleFlowStatus> articleFlowStatuses = articleFlowStatusService.findListByArticleId(article.getId());

        //为执行过该流程，或者该流程全部执行完毕
        if (ArrayUtil.isNullOrEmpty(articleFlowStatuses) || isAllFinished(articleFlowStatuses)) {
            Map<String, Object> variables = new HashMap<>();

            variables.put(FlowConsts.VAR_CONTENT_TYPE, ArticleFlowConsts.ARTICLE_CONTENT_TYPE);
            variables.put(FlowConsts.VAR_CONTENT_ID, article.getId());
            variables.put(FlowConsts.VAR_CONTENT_TITLE, article.getTitle());

            variables.put(ArticleFlowConsts.VAR_AUTHOR_ID, article.getUserId());


            try {
                RuntimeService runtimeService = FlowableUtil.getRuntimeService();
                ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(flowTemplate.getBpmnProcessKey()
                        , "article:" + article.getId(), variables);

//                ProcessInstance processInstance = runtimeService.startProcessInstanceById(flowTemplate.getBpmnProcessKey()
//                        , "article:" + article.getId(), variables);

                //流程启动成功
                if (processInstance != null) {
                    ArticleFlowStatus articleFlowStatus = new ArticleFlowStatus();
                    articleFlowStatus.setArticleId(article.getId());
                    articleFlowStatus.setFlowId(flowTemplate.getId());
                    articleFlowStatus.setFlowProcessKey(flowTemplate.getBpmnProcessKey());
                    articleFlowStatus.setFlowProcessInstanceId(processInstance.getId());
                    articleFlowStatus.setStatus(ArticleFlowStatus.STATUS_RUNNING); //执行中
                    articleFlowStatusService.save(articleFlowStatus);

                    return true;
                }
            } catch (Exception e) {
                //当流程已经线下，但是分类未取消绑定时，会出现：FlowableObjectNotFoundException 异常
                LogKit.error(e.toString(), e);
            }
        }


        //有流程正在执行中...
        //return true，让文章标识为审核中的状态
        if (articleFlowStatuses != null) {
            for (ArticleFlowStatus articleFlowStatus : articleFlowStatuses) {
                if (articleFlowStatus.isRunning()) {
                    return true;
                }
            }
        }


        return false;

    }

    private boolean isAllFinished(List<ArticleFlowStatus> articleFlowStatuses) {
        for (ArticleFlowStatus articleFlowStatus : articleFlowStatuses) {
            if (!articleFlowStatus.isFinished()) {
                return false;
            }
        }
        return true;
    }


    private Long[] getTagIds(String[] tags) {
        if (tags == null || tags.length == 0) {
            return null;
        }

        Set<String> tagset = new HashSet<>();
        for (String tag : tags) {
            tagset.addAll(StrUtil.splitToSet(tag, ","));
        }

        List<ArticleCategory> categories = categoryService.doCreateOrFindByTagString(tagset.toArray(new String[0]));
        long[] ids = categories.stream().mapToLong(value -> value.getId()).toArray();
        return ArrayUtils.toObject(ids);
    }


    /**
     * 撤销审核
     */
    public void doCancelApproval() {
        Long articleId = getIdPara();
        Article article = articleService.findById(articleId);
        if (article == null) {
            renderFailJson("当前文章不存在或已被删除.");
            return;
        }

        if (!(article.isUnderReview() || article.isRebut())) {
            renderFailJson("当前文章已发布或已被拒绝发布，无法撤销.");
            return;
        }

        List<ArticleFlowStatus> articleFlowStatuses = articleFlowStatusService.findRunningByArticleId(articleId);
        if (articleFlowStatuses != null && articleFlowStatuses.size() > 0) {
            boolean stopProcessSuccess = false;
            for (ArticleFlowStatus articleFlowStatus : articleFlowStatuses) {

                Map<String, Object> variables = new HashMap<>();
                variables.put(FlowConsts.VAR_ACTION, FlowConsts.ACTION_STATUS_STOP);

                boolean success = FlowableUtil.stopProcessInstance(articleFlowStatus.getFlowProcessInstanceId(), variables);
                if (success && !stopProcessSuccess){
                    stopProcessSuccess = true;
                }
            }

            //停止失败时，可能是审核实例已经被删除，需要手动把文章设置为草稿状态
            if (!stopProcessSuccess){
                article.setStatus(Article.STATUS_DRAFT);
                articleService.update(article);
            }
        } else {
            article.setStatus(Article.STATUS_DRAFT);
            articleService.update(article);
        }

        renderOkJson();
    }


    public void queryRunningProcessInstanceIds() {
        Long articleId = getIdPara();
        Article article = articleService.findById(articleId);
        if (article == null) {
            renderFailJson("当前文章不存在或已被删除.");
            return;
        }

        if (!(article.isUnderReview() || article.isRebut())) {
            renderFailJson("当前文章已发布或已被拒绝发布，无法撤销.");
            return;
        }

        List<ArticleFlowStatus> articleFlowStatuses = articleFlowStatusService.findRunningByArticleId(articleId);
        List<String> processInstanceIds = articleFlowStatuses.stream().map(
                        articleFlowStatus -> articleFlowStatus.getFlowProcessInstanceId())
                .collect(Collectors.toList());

        renderOkJson("processInstanceIds", processInstanceIds);
    }


    @PermissionDef(title = "查看文章分类", groupId = ArticlePermissionGroups.ARTICLE)
    @AdminMenu(text = "分类", groupId = "article", order = 2)
    public void category() {
        List<ArticleCategory> categories = categoryService.findListByType(ArticleCategory.TYPE_CATEGORY);
        SortKit.toLayer(categories);
        setAttr("categories", categories);
        long id = getParaToLong(0, 0L);
        if (id > 0 && categories != null) {
            for (ArticleCategory category : categories) {
                if (category.getId().equals(id)) {
                    setAttr("category", category);
                    setAttr("isDisplayInMenu", menuService.findFirstByRelatives("article_category", id) != null);

                    Long flowTemplateId = category.getFlowTemplateId();
                    if (flowTemplateId != null) {
                        setAttr("flowTemplate", flowTemplateService.findById(flowTemplateId));
                    }
                }
            }
        }
        initStylesAttr("artlist_");


        List<Long> departmentIds = id > 0 ? categoryService.findDepartmentIds(id) : null;
        List<Department> departments = ModelUtil.copy(departmentService.findAll());

        if (departmentIds != null && departmentIds.size() > 0) {
            for (Department department : departments) {
                if (departmentIds.contains(department.getId())) {
                    department.put("isCheck", true);
                }
            }
        }

        SortKit.toLayer(departments);
        setAttr("departments", departments);

        render("article/category_list.html");
    }


    @PermissionDef(title = "查看文章标签", groupId = ArticlePermissionGroups.ARTICLE)
    @AdminMenu(text = "标签", groupId = "article", order = 4)
    public void tag() {
        Page<ArticleCategory> page = categoryService.paginateByType(getPagePara(), getPageSizePara(), ArticleCategory.TYPE_TAG);
        setAttr("page", page);

        int categoryId = getParaToInt(0, 0);
        if (categoryId > 0) {
            setAttr("category", categoryService.findById(categoryId));
            //该分类是否显示在菜单上
            setAttr("isDisplayInMenu", menuService.findFirstByRelatives("article_category", categoryId) != null);
        }

        initStylesAttr("artlist_");
        render("article/tag_list.html");
    }


    @PermissionDef(title = "修改文章分类", groupId = ArticlePermissionGroups.ARTICLE)
    @EmptyValidate({
            @Form(name = "category.title", message = "分类名称不能为空"),
            @Form(name = "category.slug", message = "固定链接不能为空")
    })
    public void doCategorySave() {
        ArticleCategory category = getModel(ArticleCategory.class, "category");
        saveCategory(category);
    }

    private void saveCategory(ArticleCategory category) {

        Ret validRet = validateSlug(category);
        if (validRet.isFail()) {
            renderJson(validRet);
            return;
        }

        ArticleCategory existModel = categoryService.findFirstByTypeAndSlug(category.getType(), category.getSlug());
        if (existModel != null && !Objects.equals(existModel.getId(), category.getId())) {
            renderJson(Ret.fail("message", "该分类的固定连接以及被占用"));
            return;
        }

        Object id = categoryService.saveOrUpdate(category);
        categoryService.doUpdateArticleCount(category.getId());

        Long[] departmentIds = getParaValuesToLong("departmentIds");
        categoryService.doUpdateCategoryDepartmentIds((Long) id, departmentIds);

        Menu displayMenu = menuService.findFirstByRelatives("article_category", id);
        Boolean isDisplayInMenu = getParaToBoolean("displayInMenu");
        if (isDisplayInMenu != null && isDisplayInMenu) {
            if (displayMenu == null) {
                displayMenu = new Menu();
            }

            displayMenu.setUrl(category.getUrl());
            displayMenu.setText(category.getTitle());
            displayMenu.setType(Menu.TYPE_MAIN);
            displayMenu.setOrderNumber(category.getOrderNumber());
            displayMenu.setRelativeTable("article_category");
            displayMenu.setRelativeId((Long) id);

            if (displayMenu.getPid() == null) {
                displayMenu.setPid(0L);
            }

            if (displayMenu.getOrderNumber() == null) {
                displayMenu.setOrderNumber(99);
            }

            menuService.saveOrUpdate(displayMenu);
        } else if (displayMenu != null) {
            menuService.delete(displayMenu);
        }

        renderOkJson();
    }

    @PermissionDef(title = "修改文章标签", groupId = ArticlePermissionGroups.ARTICLE)
    @EmptyValidate({
            @Form(name = "category.title", message = "标签名称不能为空"),
    })
    public void doTagSave() {

        ArticleCategory tag = getModel(ArticleCategory.class, "category");

        String slug = tag.getTitle().contains(".")
                ? tag.getTitle().replace(".", "_")
                : tag.getTitle();

        //新增 tag
        if (tag.getId() == null) {
            ArticleCategory indbTag = categoryService.findFirstByTypeAndSlug(ArticleCategory.TYPE_TAG, slug);
            if (indbTag != null) {
                renderJson(Ret.fail().set("message", "该标签已经存在，不能新增。"));
                return;
            }
        }

        tag.setSlug(slug);

        Ret validRet = validateSlug(tag);
        if (validRet.isFail()) {
            validRet.put("message", validRet.getStr("message").replace("固定链接", "标签名称"));
            renderJson(validRet);
            return;
        }


        saveCategory(tag);
    }


    @PermissionDef(title = "删除文章分类", groupId = ArticlePermissionGroups.ARTICLE)
    public void doCategoryDel() {
        categoryService.deleteById(getIdPara());
        renderOkJson();
    }


    @PermissionDef(title = "文章设置", groupId = ArticlePermissionGroups.ARTICLE)
    @AdminMenu(text = "设置", groupId = "article", order = 6)
    public void setting() {
        render("article/setting.html");
    }


    @PermissionDef(title = "删除文章", groupId = ArticlePermissionGroups.ARTICLE)
    public void doDel() {
        Long id = getIdPara();
        render(articleService.deleteById(id) ? OK : FAIL);
    }


    @PermissionDef(title = "批量删除文章", groupId = ArticlePermissionGroups.ARTICLE)
    @EmptyValidate(@Form(name = "ids"))
    public void doDelByIds() {
        Set<String> idsSet = getParaSet("ids");
        render(articleService.deleteByIds(idsSet.toArray()) ? OK : FAIL);
    }

    @PermissionDef(title = "移动文章到垃圾桶", groupId = ArticlePermissionGroups.ARTICLE)
    public void doTrash() {
        Long id = getIdPara();
        render(articleService.doChangeStatus(id, Article.STATUS_TRASH) ? OK : FAIL);
    }


    @PermissionDef(title = "移动文章到草稿箱", groupId = ArticlePermissionGroups.ARTICLE)
    public void doDraft() {
        Long id = getIdPara();
        render(articleService.doChangeStatus(id, Article.STATUS_DRAFT) ? OK : FAIL);
    }


    public void rebuildIndex() {
        new Thread(() -> {
            int page = 1;
            int pagesize = 100;
            SiteModelProxy.useAllSites();
            Page<Article> articlePage = articleService._paginateByColumns(page, pagesize,
                    Columns.create("status", Article.STATUS_NORMAL), null);
            do {
                for (Article article : articlePage.getList()) {
                    ArticleSearcherFactory.getSearcher().updateArticle(article);
                }
                page++;
            } while (!articlePage.isLastPage());
            SiteModelProxy.clearUsed();
        }).start();
        renderOkJson();
    }

}
